選択範囲をMarkdown記法に変換してclip boardにcopyするPopupMenu
code:script.js
import { ScrapboxParser } from "/api/code/takker/scrapbox-parser.min.js/parser.js";
scrapbox.PopupMenu.addButton({
title: "toMD",
onClick: (text) => {
try {
const blocks = ScrapboxParser.parse(text, { hasTitle: false });
const topIndentLevel = Math.min(...blocks.map((block) => block.indent));
navigator.clipboard.writeText(
blocks.map((block) => convertSb2Md(block, topIndentLevel)).join("\n")
);
} catch (e) {
alert(Failed to copy:\n${JSON.stringify(e)});
}
},
});
code:script.js
// @ts-check
/**
* @typedef {import("../scrapbox-parser/mod.ts").Block} Block
* @typedef {import("../scrapbox-parser/mod.ts").Table} Table
* @typedef {import("../scrapbox-parser/mod.ts").Line} Line
* @typedef {import("../scrapbox-parser/mod.ts").Node} NodeType
* @typedef {import("../scrapbox-jp%2Ftypes/userscript.ts").Scrapbox} Scrapbox
*/
/** Scrapbox記法をMarkdown記法に変える
*
* @param {Block} block
* @param {number} topIndentLevel
* @return {string}
*/
function convertSb2Md(block, topIndentLevel) {
switch (block.type) {
case 'title':
return ''; // タイトルは選択範囲に入らないので無視
case 'codeBlock':
return [
block.fileName,
\n\`\`\`${getFileType(block.fileName)},
block.content,
'`\n',
].join('\n');
case 'table':
return convertTable(block);
case 'line':
return convertLine(block, topIndentLevel);
}
}
/** Table記法の変換
*
* @param {Table} table
* @return {string}
*/
function convertTable(table) {
const maxCol = Math.max(...table.cells.map(row => row.length));
return [
table.fileName,
...table.cells.flatMap((row, i) => [
`| ${row
.map(column => column.map(node => convertNode(node)).join(''))
.join(' | ')} |`,
...(i === 0 ? [|${' -- |'.repeat(maxCol)}] : []),
]),
].join('\n');
}
const INDENT = ' '; // インデントに使う文字
code:script.js
/** 行の変換
*
* @param {Line} line
* @param {number} topIndentLevel
* @return {string}
*/
function convertLine(line, topIndentLevel) {
const content = line.nodes
.map(node => convertNode(node, { section: line.indent === topIndentLevel }))
.join('')
.trim();
if (content === '') return ''; // 空行はそのまま返す
// リストを作る
if (line.indent === topIndentLevel) return content; // トップレベルの行はインデントにしない
let result = INDENT.repeat(line.indent - topIndentLevel - 1);
if (!/^\d+\. /.test(content)) result += '- '; // 番号なしの行は-を入れる
return result + content;
}
code:script.js
/** Nodeを変換する
*
* @param {NodeType} node
* @param {{section?:boolean}} init * @return {string}
*/
function convertNode(node, init) {
const { section = false } = init ?? {};
switch (node.type) {
case 'quote':
return > ${node.nodes.map(node => convertNode(node)).join('')};
case 'helpfeel':
return \`? ${node.text}\`;
case 'image':
case 'strongImage':
return ![image](${node.src});
case 'icon':
case 'strongIcon':
// 仕切り線だけ変換する
case 'strong':
return **${node.nodes.map(node => convertNode(node)).join('')}**;
case 'formula':
return $${node.formula}$;
case 'decoration': {
let result = node.nodes.map(node => convertNode(node)).join('');
if (node.decos.includes('/')) result = *${result}*;
// 見出しの変換
// お好みで変えて下さい
if (section) {
if (node.decos.includes('*-3')) result = # ${result}\n;
if (node.decos.includes('*-2')) result = ## ${result}\n;
if (node.decos.includes('*-1')) result = ### ${result}\n;
} else {
if (node.decos.some(deco => /\*-/.test(deco0))) { result = **${result}**;
}
}
if (node.decos.includes('~')) result = ~~${result}~~;
return result;
}
case 'code':
return \`${node.text}\`;
case 'commandLine':
return \`${node.symbol} ${node.text}\`;
case 'link':
switch (node.pathType) {
case 'root':
return [${node.href}](https://scrapbox.io${encodeURI(node.href)});
case 'relative':
//@ts-ignore declare宣言が使えないため、scrapboxに型定義をつけられない
scrapbox.Project.name
}/${encodeURI(node.href)})`;
default:
return node.content === ''
? ${node.href}
: [${node.content}](${encodeURI(node.href)});
}
case 'googleMap':
return [${node.place}](${node.url});
case 'hashTag':
//@ts-ignore declare宣言が使えないため、scrapboxに型定義をつけられない
return [#${node.href}](https://scrapbox.io/${scrapbox.Project.name}/${node.href});
case 'numberList':
return `${node.number}. ${node.nodes
.map(node => convertNode(node))
.join('')}`;
case 'blank':
case 'plain':
return node.text;
}
}